package com.bensonbasement.snorflux.client;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.List;

import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import com.bensonbasement.snorflux.client.WorldUpdateManager.UpdateHandler;
import com.bensonbasement.snorflux.client.states.ConnectingState;
import com.bensonbasement.snorflux.client.states.DisconnectedState;
import com.bensonbasement.snorflux.memory.Memory;
import com.bensonbasement.snorflux.memory.RBigEntity;
import com.bensonbasement.snorflux.memory.RLevel;
import com.bensonbasement.snorflux.memory.RSmallEntity;
import com.bensonbasement.snorflux.memory.RSpace;
import com.bensonbasement.snorflux.messages.fromclient.IFromClientMessage;
import com.bensonbasement.snorflux.messages.fromserver.JoinedMessage;
import com.bensonbasement.snorflux.messages.fromserver.ServerTimeMessage;
import com.bensonbasement.snorflux.messages.fromserver.WorldTimeMessage;
import com.bensonbasement.snorflux.utils.Coords;

public class SnorfluxClient implements ChatPanel.ChatPanelHandler, SnorfluxClientHandler.Callback, RLevel.RLevelSizeChangeListener {

	public interface ClientStatusListener {
		public void clientStatusChanged();
	}
	
	public static final int TILE_WIDTH = 32; // Width/height of tiles in pixels.
	public static final String FOGIMAGE = "fog2";
	
	private SnorfluxClientHandler clientHandler;
	private ClientState state;
	
	private ClientStatusListener listener;
	
	private ImageWarehouse warehouse = ImageWarehouse.getInstance();
	
	private JPanel gamePanel;
	private JPanel mapPanel;
	private ChatPanel chatPanel;
	
	// TODO: Change this to recreate or empty itself when a new game begins:
	private WorldUpdateManager worldUpdateManager;
	private PlayerListModel playerListModel;
//	private RBigEntity controlledActor = null;
	private RLevel level = null;
	// TODO: Change this to receive a Memory from the server when the game
	// starts.
	private Memory memory = null;
	
	public SnorfluxClient(ClientStatusListener listener, InetSocketAddress address, String playerName) {
		
		this.listener = listener;
		
		this.clientHandler = new SnorfluxClientHandler(this);
		
		this.worldUpdateManager = new WorldUpdateManager(new UpdateHandler() {
			@Override
			public void handleWorldTimeMessage(WorldTimeMessage message) {
				state.handleWorldTimeMessage(message);
			}
			@Override
			public void outOfUpdates() {
				mapPanel.repaint();
			}
		});
		
		this.mapPanel = new JPanel() {
			protected void paintComponent(Graphics g) {
				state.drawMapPanel(g);
			};
		};
		mapPanel.setFocusable(true);
		mapPanel.addKeyListener(new KeyListener() {
			@Override
			public void keyPressed(KeyEvent e) {
				state.keyPressed(e);
			}
			@Override
			public void keyReleased(KeyEvent e) {
				state.keyReleased(e);
			}
			@Override
			public void keyTyped(KeyEvent e) {
				state.keyTyped(e);
			}
		});
		mapPanel.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				mapPanel.requestFocusInWindow();
				// TODO: Pass info to state.
			}
		});
		
		this.chatPanel = new ChatPanel(this);
		this.chatPanel.setEnabled(false);

		this.playerListModel = new PlayerListModel();
		
		JList playerList = new JList(playerListModel);
		playerList.setFocusable(false);
		
		JPanel playerListPanel = new JPanel(new BorderLayout());
		playerListPanel.add(new JLabel("Players:"),BorderLayout.NORTH);
		playerListPanel.add(playerList,BorderLayout.CENTER);
		
		JPanel chatAndListArea = new JPanel(new BorderLayout());
		chatAndListArea.add(chatPanel,BorderLayout.CENTER);
		chatAndListArea.add(playerListPanel,BorderLayout.EAST);
		
		gamePanel = new JPanel(new BorderLayout());
		gamePanel.add(mapPanel,BorderLayout.CENTER);
		gamePanel.add(chatAndListArea,BorderLayout.SOUTH);
				
		changeState(new ConnectingState(this));
		worldUpdateManager.start();
		chatPanel.postMessage("Connecting to "+address.toString()+"...");
		clientHandler.connect(address, playerName);
	}
	
	public void disconnect() {
		clientHandler.disconnect();
	}
	
	@Override
	public void invokeLater(Runnable runnable) {
		SwingUtilities.invokeLater(runnable);
	}
	
	public void focusMapPanel() {
		mapPanel.requestFocusInWindow();
	}
	
	@Override
	public void handleTextEntered(String text) {
		state.handleTextEntered(text);
	}
	
	@Override
	public void handleTextCanceled() {
		state.handleTextCanceled();
	}
	
	@Override
	public void handleConnected(JoinedMessage message) {
		state.handleConnected(message);
		listener.clientStatusChanged();
	}
	
	@Override
	public void handleDisconnected() {
		state.handleDisconnected();
		listener.clientStatusChanged();
	}
	
	@Override
	public void handleServerTimeMessage(ServerTimeMessage message) {
		state.handleServerTimeMessage(message);
	}
	
	@Override
	public void handleWorldTimeMessage(WorldTimeMessage message) {
		worldUpdateManager.queueUpdate(message);
	}
	
	public JPanel getGamePanel() {
		return gamePanel;
	}
	
	public boolean isFinished() {
		// HACK
		return (this.state.getClass() == DisconnectedState.class);
	}
	
	public void setLevel(RLevel level) {
		if (level != this.level) {
			if (this.level != null) {
				this.level.removeSizeChangeListener(this);
			}
			this.level = level;
			if (level != null) {
				this.sizeChanged(level.getKnownWidth(),level.getKnownHeight());
				level.addSizeChangeListener(this);
			}
			else {
				this.sizeChanged(0,0);
			}
		}
	}
	
	@Override
	public void sizeChanged(int newWidth, int newHeight) {
		mapPanel.setMinimumSize(new Dimension(newWidth*TILE_WIDTH,newHeight*TILE_WIDTH));
	}
	
	private void changeState(ClientState newState) {
		this.state = newState;
		newState.initialize();
	}
	
	public static abstract class ClientState implements KeyListener, ChatPanel.ChatPanelHandler {
		
		protected final SnorfluxClient client;
		
		public ClientState(SnorfluxClient client) {
			this.client = client;
		}

		public abstract void handleConnected(JoinedMessage message);
		public abstract void handleDisconnected();
		
		/**
		 * Called when the state is set.
		 */
		public void initialize() {}
		
		protected final JPanel getMapPanel() {
			return client.mapPanel;
		}
		
		protected final ChatPanel getChatPanel() {
			return client.chatPanel;
		}
		
		protected final Memory getMemory() {
			return client.memory;
		}
		
		protected final void setMemory(Memory memory) {
			client.memory = memory;
		}
		
		protected final RLevel getLevel() {
			return client.level;
		}
		
		protected final void setLevel(RLevel level) {
			client.setLevel(level);
		}
		
		protected final PlayerListModel getPlayerListModel() {
			return client.playerListModel;
		}
		
		protected final ImageWarehouse getWarehouse() {
			return client.warehouse;
		}
		
		protected final WorldUpdateManager getWorldUpdateManager() {
			return client.worldUpdateManager;
		}
		
		protected final void sendMessage(IFromClientMessage message) {
			client.clientHandler.sendMessage(message);
		}
		
		protected final void changeState(ClientState newState) {
			client.changeState(newState);
		}
		
		public void drawMapPanel(Graphics g) {
			g.setColor(Color.BLACK);
			g.fillRect(0,0,getMapPanel().getWidth(),getMapPanel().getHeight());
			if (client.level != null) {
				drawLevel(g,client.level);
			}
		}
		
		protected void drawLevel(Graphics g, RLevel level) {
			Collection<RSpace> spacesToDraw = level.getAllSpaces();
			for (RSpace space : spacesToDraw) {
				drawFullSpace(g, space);
			}
		}
		
		protected void drawFullSpace(Graphics g, RSpace space) {
			Coords drawCoords = this.getCoordsInPanel(space.getCoords());
			drawSpaceItself(g,space,drawCoords);
			drawSmallEntity(g,space,drawCoords);
			drawBigEntity(g,space,drawCoords);
			drawFog(g,space,drawCoords);
		}
		
		protected void drawSpaceItself(Graphics g, RSpace space, Coords drawCoords) {
			Image spaceImage = getWarehouse().getImage(space.getGlyph());
			g.drawImage(spaceImage, drawCoords.getX(), drawCoords.getY(), null);
		}
		
		protected void drawSmallEntity(Graphics g, RSpace space, Coords drawCoords) {
			// smallEntity
			List<RSmallEntity> smallEntities = space.getSmallEntities();
			if (!smallEntities.isEmpty()) {
				// The "top" entity is the last entity, e.g. the most recently added.
				RSmallEntity topEntity = smallEntities.get(smallEntities.size()-1);
				Image smallEntityImage = getWarehouse().getImage(topEntity.getGlyph());
				g.drawImage(smallEntityImage, drawCoords.getX(), drawCoords.getY(), null);
			}
		}
		
		protected void drawBigEntity(Graphics g, RSpace space, Coords drawCoords) {
			// bigEntity
			RBigEntity bigEntity = space.getBigEntity();
			if (bigEntity != null) {
				Image bigEntityImage = getWarehouse().getImage(bigEntity.getGlyph());
				g.drawImage(bigEntityImage, drawCoords.getX(), drawCoords.getY(), null);
			}
		}
		
		protected void drawFog(Graphics g, RSpace space, Coords drawCoords) {
			// fog of war
			if (! space.getMemory().isSpaceVisible(space.getWorldElementID())) {
				Image fogImage = client.warehouse.getImage(FOGIMAGE);
				g.drawImage(fogImage, drawCoords.getX(), drawCoords.getY(), null);
			}
		}
		
		protected final Coords getCoordsInPanel(Coords coordsInLevel) {
			return coordsInLevel.times(TILE_WIDTH);
		}
		
		protected final Coords getCoordsInLevel(Coords coordsInPanel) {
			return coordsInPanel.integerDivide(TILE_WIDTH);
		}
		
		public abstract void handleServerTimeMessage(ServerTimeMessage message);
		public abstract void handleWorldTimeMessage(WorldTimeMessage message);
	}
}
